package com.metrink.croquet; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.concurrent.TimeUnit; import javax.servlet.DispatcherType; import org.apache.wicket.guice.GuiceWebApplicationFactory; import org.apache.wicket.protocol.http.WicketFilter; import org.apache.wicket.protocol.ws.jetty9.Jetty9WebSocketFilter; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.component.AbstractLifeCycle.AbstractLifeCycleListener; import org.eclipse.jetty.util.component.LifeCycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.name.Names; import com.google.inject.persist.PersistFilter; import com.google.inject.util.Providers; import com.metrink.croquet.hibernate.DataSourceHibernateModule; import com.metrink.croquet.hibernate.PersistanceUnitHibernateModule; import com.metrink.croquet.hibernate.QueryRunnerModule; import com.metrink.croquet.inject.CroquetWicketModule; import com.metrink.croquet.modules.ManagedModule; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * This is the main class of Croquet. * * You must create an instance of this class to setup and run the framework. * @param <T> The class used when loading the settings YAML */ public class CroquetWicket<T extends WicketSettings> { protected static final EnumSet<DispatcherType> DISPATCHER_TYPES = EnumSet.of(DispatcherType.ASYNC, DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE); private static final Logger LOG = LoggerFactory.getLogger(CroquetWicket.class); private volatile CroquetStatus status = CroquetStatus.STOPPED; private Injector injector; private final List<Class<? extends ManagedModule>> managedModules = new ArrayList<Class<? extends ManagedModule>>(); private final List<Module> guiceModules = new ArrayList<Module>(); private final T settings; private Server jettyServer; private final ServletContextHandler sch; /** * Constructs a Croquet instance given the arguments pass into the program. The {@link WicketSettings} type T is passed * as the first argument to this constructor to avoid ugly reflection. * * @param clazz the settings class * @param args the arguments passed into the program. */ CroquetWicket(final Class<T> clazz, final T settings) { this.settings = settings; sch = new ServletContextHandler(ServletContextHandler.SESSIONS); // add the croquet module to our list of guice modules guiceModules.add(new CroquetWicketModule<T>(clazz, settings)); // The user can pick between: no db, a persistence.xml file, or using application.yml file. // If you're not using a db, then we skip this. // If you're using the application.yml file, then we install the DataSource Hibernate module. // If you're using the persistence.xml file, then we install the JpaPersistenceModule. if(settings.getDatabaseSettings().getNotUsed()) { LOG.info("Not configuring a database. " + "If you get an error about no binding for an EntityManager, you need to configure a database."); } else if(settings.getDatabaseSettings().getPersistenceUnit() == null) { LOG.info("Using YAML file to configure Hibernate"); final DataSourceFactory dataSourceFactory = new DataSourceFactory(settings.getDatabaseSettings()); guiceModules.add(new DataSourceHibernateModule(dataSourceFactory)); guiceModules.add(new QueryRunnerModule(dataSourceFactory)); } else { LOG.info("Using persistence.xml file to configure Hibernate"); guiceModules.add(new PersistanceUnitHibernateModule()); } // this sets the name of the peristence unit // we need to jump through these hoops because it has to be // null when we're doing a unit test guiceModules.add(new AbstractModule() { @Override protected void configure() { bind(String.class).annotatedWith(Names.named("jpa-unit-name")) .toProvider(getPUNameProvider()); } }); } /** * Provides the name of the persistence unit. * @return the name of the persistence unit. */ protected Provider<String> getPUNameProvider() { final String puName = settings.getDatabaseSettings().getPersistenceUnit(); return Providers.<String>of(puName == null ? "croquet" : puName); } /** * Creates and sets the injector. */ protected void createInjector() { // create our injector injector = Guice.createInjector(guiceModules); } /** * Gets the injector. * @return injector. */ protected Injector getInjector() { return injector; } /** * Creates and starts all the managed modules. * @return a list of the managed module instances. */ protected List<ManagedModule> createAndStartModules() { final List<ManagedModule> managedModuleInstances = new ArrayList<>(); // create and start each managed module for(final Class<? extends ManagedModule> module:managedModules) { final ManagedModule mm = injector.getInstance(module); managedModuleInstances.add(mm); mm.start(); } return managedModuleInstances; } /** * Returns the parsed settings. * @return the parsed settings. */ public T getSettings() { return settings; } /** * Gets the status of the Croquet application. * @return the status of the Croquet application. */ public CroquetStatus getStatus() { return status; } /** * Sets the status of Corquet. * @param status the status. */ protected void setStatus(final CroquetStatus status) { this.status = status; } /** * Starts the Croquet framework. */ @SuppressFBWarnings("DM_EXIT") public void run() { status = CroquetStatus.STARTING; // create the injector createInjector(); // configure the Jetty server jettyServer = configureJetty(settings.getPort()); // add a life-cycle listener to remove drop the PID jettyServer.addLifeCycleListener(new AbstractLifeCycleListener() { @Override public void lifeCycleStarted(final LifeCycle event) { final String pidFile = settings.getPidFile(); // when a pid-file is configured, drop it upon successfully binding to the port. if (pidFile != null) { LOG.info("Dropping PID file: {}", pidFile); // the PidManager will automatically remove the pid file on shutdown new PidManager(pidFile).dropPidOrDie(); } else { LOG.warn("No PID file specified, so not file will be dropped. " + "Set a file via code or in the config file to drop a pid file."); } } }); // start the server try { jettyServer.start(); //CHECKSTYLE:OFF the only exception that is thrown } catch (final Exception e) { //CHECKSTYLE:ON LOG.error("Error starting Jetty: {}", e.getMessage(), e); System.err.println("Error starting Jetty: " + e.getMessage()); System.exit(-1); throw new RuntimeException(e); // throw this so the whole app stops } // create and start the modules final List<ManagedModule> managedModuleInstances = createAndStartModules(); // install the shutdown hook Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { status = CroquetStatus.STOPPING; try { jettyServer.stop(); //CHECKSTYLE:OFF the only exception that is thrown } catch (final Exception e) { //CHECKSTYLE:ON LOG.error("Error stopping Jetty: {}", e.getMessage(), e); } // go through the modules stopping them for(final ManagedModule module:managedModuleInstances) { module.stop(); } status = CroquetStatus.STOPPED; LOG.info("Croquet has stopped"); } }); status = CroquetStatus.RUNNING; LOG.info("Croquet is running on port {}", settings.getPort()); } /** * Adds a module to the Guice injector. * @param module the module to add to the Guice injector. */ public void addGuiceModule(final Module module) { guiceModules.add(module); } /** * Adds a {@link ManagedModule} to the list of modules to be start. * * Modules are created by Guice so you can inject any dependencies. * Modules are started in the order that they are added. * * @param module the module to add. */ public void addManagedModule(final Class<? extends ManagedModule> module) { managedModules.add(module); } /** * Gets the {@link ServletContextHandler} for Jetty. * @return the {@link ServletContextHandler} Jetty uses. */ public ServletContextHandler getServletContextHandler() { return sch; } /** * Function used to configure Jetty and return a Server instance. * @param port the port to run Jetty on. * @return the {@link Server} instance. */ protected Server configureJetty(final int port) { final Server server = new Server(); final ServerConnector connector = new ServerConnector(server); // TODO: make all of this configurable connector.setIdleTimeout((int)TimeUnit.HOURS.toMillis(1)); connector.setSoLingerTime(-1); connector.setPort(port); server.addConnector(connector); // set the injector as an attribute in the context sch.setAttribute("guice-injector", injector); // prevent the JSESSIONID from getting set via a URL argument sch.setInitParameter("org.eclipse.jetty.servlet.SessionIdPathParameterName", "none"); // add the font mime type by default sch.getMimeTypes().addMimeMapping("woff", "application/x-font-woff"); // if we're using a database, then install the filter if(!settings.getDatabaseSettings().getNotUsed()) { // setup a FilterHolder for the Guice Persistence final FilterHolder persistFilter = new FilterHolder(injector.getInstance(PersistFilter.class)); // add the filter to the context sch.addFilter(persistFilter, "/*", DISPATCHER_TYPES); } // setup a FilterHolder for WebSockets final FilterHolder webSocketFilter = new FilterHolder(Jetty9WebSocketFilter.class); // set the app factor as the Guice Web App Factory webSocketFilter.setInitParameter("applicationFactoryClassName", GuiceWebApplicationFactory.class.getName()); // tell the filter to use the injector in the context instead of making a new one webSocketFilter.setInitParameter("injectorContextAttribute", "guice-injector"); // setup the filter mapping webSocketFilter.setInitParameter(WicketFilter.FILTER_MAPPING_PARAM, "/*"); webSocketFilter.setInitParameter("configuration", "deployment"); // add the filter to the context sch.addFilter(webSocketFilter, "/*", DISPATCHER_TYPES); // add the default servlet as Guice & Wicket will take care of everything for us sch.addServlet(DefaultServlet.class, "/*"); server.setHandler(sch); return server; } }